

#include "levelwalker.h"
#pragma warning(disable: 4996)

#include "resource.h"

#define _WIN32_WINNT 0x501

#include <windows.h>
#include <commctrl.h>
#include <atlbase.h>
#include <math.h>
#include <dbghelp.h>

#undef min
#undef max

#include <vector>
#include <set>
#include <map>
#include <string>
#include <algorithm>
#include <assert.h>

#include <psapi.h>

#include <GL/gl.h>
#include <GL/glu.h>
#include <IL/il.h>
#include <IL/ilut.h>

#include <ode/ode.h>

#include "DeleDAPI.h"

#pragma comment(lib, "comctl32.lib")
#pragma comment(lib, "comdlg32.lib")
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "opengl32.lib")
#pragma comment(lib, "glu32.lib")


#define STRINGIZE2(x) #x
#define STRINGIZE(x) STRINGIZE2(x)

inline bool operator==(Float3 const &a, Float3 const &b) {
  return (a.x_ == b.x_ && a.y_ == b.y_ && a.z_ == b.z_);
}

inline bool operator<(Float3 const &a, Float3 const &b) {
  return (a.x_ < b.x_) || ((a.x_ == b.x_) && ((a.y_ < b.y_) || ((a.y_ == b.y_) && (a.z_ < b.z_))));
}

inline Float3 operator-(Float3 const &a, Float3 const &b) {
  return Float3(a.x_ - b.x_, a.y_ - b.y_, a.z_ - b.z_);
}

inline Float3 operator*(Float3 const &a, float b) {
  return Float3(a.x_*b, a.y_*b, a.z_*b);
}

inline Float3 operator+(Float3 const &a, Float3 const &b) {
  return Float3(a.x_ + b.x_, a.y_ + b.y_, a.z_ + b.z_);
}

inline Float3& operator+=(Float3 &a, Float3 const &b) {
  a.x_ += b.x_;
  a.y_ += b.y_;
  a.z_ += b.z_;
  return a;
}

inline Float3 &operator*=(Float3 &a, float b) {
  a.x_ *= b;
  a.y_ *= b;
  a.z_ *= b;
  return a;
}

inline float length_squared(Float3 const &f) {
  float l2 = f.x_*f.x_ + f.y_*f.y_ + f.z_*f.z_;
  return l2;
}

inline float length(Float3 const &f) {
  return sqrtf(length_squared(f));
}

inline Float3 &normalize(Float3 &f) {
  f *= 1.0f/length(f);
  return f;
}

inline void maximize(Float3 &a, Float3 const &b) {
  a.x_ = std::max(a.x_, b.x_);
  a.y_ = std::max(a.y_, b.y_);
  a.z_ = std::max(a.z_, b.z_);
}

inline void minimize(Float3 &a, Float3 const &b) {
  a.x_ = std::min(a.x_, b.x_);
  a.y_ = std::min(a.y_, b.y_);
  a.z_ = std::min(a.z_, b.z_);
}

inline float dot(Float3 const &a, Float3 const &b) {
  return a.x_*b.x_ + a.y_*b.y_ + a.z_*b.z_;
}

Float3 mix(Float3 const &a, Float3 const &b, int i) {
  return Float3(
      (i & 1) ? b.x_ : a.x_,
      (i & 2) ? b.y_ : a.y_,
      (i & 4) ? b.z_ : a.z_);
}

void matrix_multiply(float *res, float const *a, float const *b) {
  glPushMatrix();
  glLoadMatrixf(b);
  glMultMatrixf(a);
  glGetFloatv(GL_MODELVIEW_MATRIX, res);
  glPopMatrix();
}

void make_plane(float a, float b, float c, float d, Float3 *oNormal, float *oDistance)
{
  *oNormal = Float3(a, b, c);
  float l = 1.0f / length(*oNormal);
  *oNormal *= l;
  *oDistance = d * l;
}

void get_camera_frustum(Float3 *frustumPoints, float *frustumDistances) {
  float m[4][4], p[4][4], mvp[4][4];
  glGetFloatv(GL_PROJECTION_MATRIX, (GLfloat *)p);
  glGetFloatv(GL_MODELVIEW_MATRIX, (GLfloat *)m);
  matrix_multiply((GLfloat *)mvp, (GLfloat *)m, (GLfloat *)p);
  //  think of them as columns in a row-major matrix with row vertices on the left
  int columns[] = {
    3, -3, 2, -2, 1, -1,
  };
  for (int i = 0; i < 6; ++i) {
    int c1 = columns[i] - 1;
    float f1 = 1;
    if (c1 < 0) {
      c1 = -c1 - 2;
      f1 = -1;
    }
    make_plane(
        mvp[0][c1] * f1 + mvp[0][3],
        mvp[1][c1] * f1 + mvp[1][3],
        mvp[2][c1] * f1 + mvp[2][3],
        mvp[3][c1] * f1 + mvp[3][3],
        frustumPoints+i,
        frustumDistances+i);
  }
}

bool aabb_frustum_visible(Float3 const &aabb_min, Float3 const &aabb_max, Float3 const *normals, float const *distances) {
  Float3 points[8];
  for (int j = 0; j < 8; ++j) {
    points[j] = mix(aabb_min, aabb_max, j);
  }
  for (int i = 0; i < 6; ++i) {
    bool inview = false;
    for (int j = 0; j < 8; ++j) {
      float d1 = dot(points[j], normals[i]) + distances[i];
      if (d1 > 0) {
        inview = true;
        break;
      }
    }
    if (!inview)
      return false;
  }
  return true;
}


#if !defined(M_PI)
#define M_PI 3.14159265358979
#endif

#define GL_TEXTURE0 0x84C0
#define GL_TEXTURE1 0x84C1
#define GL_TEXTURE2 0x84C2
#define GL_TEXTURE3 0x84C3
#define GL_TEXTURE4 0x84C4
#define GL_TEXTURE5 0x84C5
#define GL_TEXTURE6 0x84C6
#define GL_TEXTURE7 0x84C7
#define GL_ACTIVE_TEXTURE 0x84E0
#define GL_COMBINE 0x8570
#define GL_COMBINE_RGB 0x8571
#define GL_COMBINE_ALPHA 0x8572
#define GL_SOURCE0_RGB 0x8580
#define GL_SOURCE1_RGB 0x8581
#define GL_SOURCE2_RGB 0x8582
#define GL_SOURCE0_ALPHA 0x8588
#define GL_SOURCE1_ALPHA 0x8589
#define GL_SOURCE2_ALPHA 0x858A
#define GL_OPERAND0_RGB 0x8590
#define GL_OPERAND1_RGB 0x8591
#define GL_OPERAND2_RGB 0x8592
#define GL_OPERAND0_ALPHA 0x8598
#define GL_OPERAND1_ALPHA 0x8599
#define GL_OPERAND2_ALPHA 0x859A
#define GL_RGB_SCALE 0x8573
#define GL_ADD_SIGNED 0x8574
#define GL_INTERPOLATE 0x8575
#define GL_SUBTRACT 0x84E7
#define GL_CONSTANT 0x8576
#define GL_PRIMARY_COLOR 0x8577
#define GL_PREVIOUS 0x8578
#define GL_DOT3_RGB 0x86AE
#define GL_DOT3_RGBA 0x86AF
#define GL_CLAMP_TO_BORDER 0x812D
#define GL_GENERATE_MIPMAP 0x8191
#define GL_SAMPLES_PASSED_ARB 0x8914
#define GL_QUERY_COUNTER_BITS_ARB 0x8864
#define GL_CURRENT_QUERY_ARB 0x8865
#define GL_QUERY_RESULT_ARB 0x8866
#define GL_QUERY_RESULT_AVAILABLE_ARB 0x8867

void (__stdcall *glActiveTexture)(GLenum);
void (__stdcall *glMultiTexCoord2f)(GLenum, GLfloat, GLfloat);

void (__stdcall *glGenQueriesARB)(GLsizei n, GLuint *ids);
void (__stdcall *glDeleteQueriesARB)(GLsizei n, const GLuint *ids);
void (__stdcall *glBeginQueryARB)(GLenum target, GLuint id);
void (__stdcall *glEndQueryARB)(GLenum target);
void (__stdcall *glGetQueryObjectuivARB)(GLuint id, GLenum pname, GLuint *params);

HMODULE hModule;
HWND hDialog;
HHOOK hHook;
HWND hMainWindow;

struct Settings {
  int radius;
  int height;
};

static Settings settings = {
  10,
  100,
};
static bool settingsDirty = false;

template<typename T >
void VisitSettings(Settings & set, T & t)
{
  t.doInt(IDC_RADIUS, "radius", set.radius);
  t.doInt(IDC_HEIGHT, "height", set.height);
}

#pragma warning (disable: 4244) //  loss of precision ll -> d

static double ReadTimer() {
  static double factor = 0;
  static long long baseline = 0;
  if (factor == 0) {
    long long ll;
    ::QueryPerformanceFrequency((LARGE_INTEGER *)&ll);
    factor = ll;
    factor = 1.0 / factor;
    ::QueryPerformanceCounter((LARGE_INTEGER *)&baseline);
  }
  long long ret;
  ::QueryPerformanceCounter((LARGE_INTEGER *)&ret);
  return (double)ret * factor;
}

static void GetSettingsFilename(std::string & str)
{
  str = ".\\levelwalker.ini";
  char name[512+32];
  if (::GetModuleFileName(::GetModuleHandle(0), name, 512) < 511) {
    char * s = strrchr(name, '\\');
    if (s) {
      strcpy(s+1, "Plugins\\levelwalker.ini");
      str = name;
    }
  }
}

static void GetTexturesFilename(std::string & str)
{
  str = ".";
  char name[512+32];
  if (::GetModuleFileName(::GetModuleHandle(0), name, 512) < 511) {
    char * s = strrchr(name, '\\');
    if (s) {
      strcpy(s+1, "Textures");
      str = name;
    }
  }
}

template<typename T> class vector2 : public std::vector<T> {
  public:
    void append(T const * src, size_t cnt) {
      size_t s = size();
      resize(s+cnt);
      std::copy(src, src+cnt, &(*this)[s]);
    }
};

static char legal_character(char ch)
{
  //  I'm pretty draconian on what I allow in object names
  if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
      (ch >= '0' && ch <= '9') || ch == '-') {
    return ch;
  }
  return '_';
}

static std::string convert_name(std::string i)
{
  std::string ret;
  ret.resize(i.size());
  std::transform(i.begin(), i.end(), ret.begin(), legal_character);
  return ret;
}

size_t gLocalProgressMax, gLocalProgress, gGlobalProgress, gGlobalProgressMax;

static void UpdateTask(std::string const & str, size_t localProgress)
{
  gLocalProgressMax = localProgress;
  gLocalProgress = 0;
  ::SetDlgItemText(hDialog, IDC_STATUSTEXT, str.c_str());
}

static void UpdateProgress(size_t n, size_t m)
{
  gGlobalProgress = n;
  gGlobalProgressMax = m;
  double a = 1000.0 * n / m;
  short p = (short)a;
  ::SendDlgItemMessage(hDialog, IDC_PROGRESS, PBM_SETRANGE, 0, MAKELPARAM(0, 1000));
  ::SendDlgItemMessage(hDialog, IDC_PROGRESS, PBM_SETPOS, p, 0);
}

static void LocalProgress(size_t n, size_t m)
{
  gLocalProgress = (size_t)((double)n / m * gLocalProgressMax);
  short p = (short)(1000.0 * (gGlobalProgress + gLocalProgress) / gGlobalProgressMax);
  ::SendDlgItemMessage(hDialog, IDC_PROGRESS, PBM_SETPOS, p, 0);
}

//todo: hack alert! The scene name is in the title of the main window...
static std::string GetSceneName()
{
  char str[512];
  ::GetWindowText(hMainWindow, str, sizeof(str));
  str[511] = 0;
  char * ptr = strstr(str, ".dxs");
  if (ptr == NULL) {
    HWND cur = hDialog;
    while (cur = ::GetWindow(cur, GW_HWNDNEXT)) {
      ::GetWindowText(cur, str, sizeof(str));
      str[511] = 0;
      ptr = strstr(str, ".dxs");
      if (ptr != NULL) {
        goto found_it;
      }
    }
    return "unnamed";
  }
found_it:
  *ptr = 0;
  while (ptr > str && ptr[-1] != ' ' && ptr[-1] != '\t' && ptr[-1] != '\\') {
    --ptr;
  }
  return ptr;
}


static void ThrowHresult(char const * func, char const * file, int line, HRESULT h)
{
  static char buf[4096];
  sprintf(buf, "Error walking level: 0x%08x\n%s:%d: %s\n", h, file, line, func);
  buf[4095] = 0;
  throw buf;
}

static ATOM glClass;

class WalkWindowBase {
  public:
    virtual ~WalkWindowBase() {}
    virtual bool InhibitDelete() = 0;
    virtual void DoKeyDown(WPARAM wParam, LPARAM lParam) = 0;
    virtual void DoKeyUp(WPARAM wParam, LPARAM lParam) = 0;
    virtual void DoMouseDown(WPARAM wParam, LPARAM lParam) = 0;
    virtual void DoMouseUp(WPARAM wParam, LPARAM lParam) = 0;
    virtual void DoMouseMove(WPARAM wParam, LPARAM lParam) = 0;
    virtual void DoPaint(WPARAM wParam, LPARAM lParam) = 0;
    virtual void DoActivate(WPARAM wParam, LPARAM lParam) = 0;
    virtual void DoTimer(WPARAM wParam, LPARAM lParam) = 0;
};

WalkWindowBase *curWalkWindow_;


std::map<std::string, GLuint> textureCache_;
std::map<std::string, GLuint> smallTextureCache_;
bool hirez_ = true;

struct TextureInts {
  GLuint texture;
  GLuint thumbnail;
};
static TextureInts LoadTexture(char const *path)
{
  std::string s(path);
  std::transform(s.begin(), s.end(), s.begin(), tolower);
  std::replace(s.begin(), s.end(), '\\', '/');
  if (textureCache_.find(s) != textureCache_.end()) {
    TextureInts ret;
    ret.texture = textureCache_[s];
    ret.thumbnail = smallTextureCache_[s];
    return ret;
  }
  std::string tfn;
  GetTexturesFilename(tfn);
  s = tfn + "\\" + s;
  //  actually load a texture
  GLuint tex = 0;
  GLuint smallTex = 0;
  ILuint img = ilGenImage();
  ilBindImage(img);
  ilEnable(IL_ORIGIN_SET);
  ilOriginFunc(IL_ORIGIN_UPPER_LEFT);

  if (ilLoadImage(s.c_str())) {

    glGenTextures(1, &tex);
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, tex);
    glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
    ILint wid, hei, byt;
    ilGetIntegerv(IL_IMAGE_WIDTH, &wid);
    ilGetIntegerv(IL_IMAGE_HEIGHT, &hei);
    ilGetIntegerv(IL_IMAGE_FORMAT, &byt);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, wid, hei, 0, byt, GL_UNSIGNED_BYTE, ilGetData());
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    if (wid * hei < 1024) {
      //  if the texture is less than one page, don't create a thumbnail
      smallTex = tex;
    }
    else {
      glGenTextures(1, &smallTex);
      glBindTexture(GL_TEXTURE_2D, smallTex);
      glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
      iluImageParameter(ILU_FILTER, ILU_SCALE_BELL);
      iluScale((wid+7)/8, (hei+7)/8, 1);
      glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, (wid+7)/8, (hei+7)/8, 0, byt, GL_UNSIGNED_BYTE, ilGetData());
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    }
    assert(!glGetError());
  }
  ilDeleteImage(img);
  ilBindImage(0);
  textureCache_[s] = tex;
  smallTextureCache_[s] = smallTex;
  TextureInts ret;
  ret.texture = tex;
  ret.thumbnail = smallTex;
  return ret;
}



class Material {
  public:
    Material(IScene *scene, int id) {
      scene_ = scene;
      id_ = id;
//      list_ = ::glGenLists(1);
      //  load necessary texture objects first
      //  set up the material mode
      IMaterial *mat = scene->materials()->item(id);
      IInterface<ILayer *> *layers = mat->layers();
      size_t n_layers = layers->count();
      for (size_t i = 0; i < n_layers; ++i) {
        ILayer *l = layers->item(i);
        TextureInts tex;
        //  textured or light mapped?
        if (l->type() == 0) {
          tex = LoadTexture(l->texture());
        }
        else if (l->type() == 2) {
          tex = LoadTexture(l->texture());
        }
        else {
          tex.texture = 0;
          tex.thumbnail = 0;
        }
        textures_.push_back(tex);
      }
//      ::glNewList(list_, GL_COMPILE);
//      setupTextures();
//      ::glActiveTexture(GL_TEXTURE0);
//      ::glEndList();
    }
    virtual ~Material() {
//      ::glDeleteLists(list_, 1);
    }
    virtual void Apply(bool hirez) {
//      ::glCallList(list_);
      setupTextures(hirez && hirez_);
    }
    void setupTextures(bool hirez) {
      IMaterial *mat = scene_->materials()->item(id_);
      IInterface<ILayer *> *layers = mat->layers();
      size_t n_layers = layers->count();
      for (size_t i = 0; i < n_layers; ++i) {
        glActiveTexture(GL_TEXTURE0 + (unsigned int)i);
        ILayer *l = layers->item(i);
        static float black[4] = { 0, 0, 0, 0 };
        glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, black);
        glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
        switch (l->blend()) {
          case 0: //  inactive
            //  add a black color
            glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_ADD);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_CONSTANT);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_CONSTANT);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_PREVIOUS);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, GL_PREVIOUS);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA);
            break;
          default:
          case 1: //  replace
            glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_REPLACE);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_TEXTURE);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA);
            break;
          case 2: //  add
            glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_ADD);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_TEXTURE);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_PREVIOUS);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, GL_PREVIOUS);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA);
            break;
          case 3: //  modulate
            glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_TEXTURE);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_PREVIOUS);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, GL_PREVIOUS);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA);
            break;
          case 4: //  alphablend
            glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_INTERPOLATE);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_TEXTURE);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_PREVIOUS);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, GL_PREVIOUS);
            glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA);
            break;
        }
        if (l->type() == 0) {   //  texture layer type
          ::glBindTexture(GL_TEXTURE_2D, hirez ? textures_[i].texture : textures_[i].thumbnail);
          ::glEnable(GL_TEXTURE_2D);
        }
        else if (l->type() == 2) {    //  lightmap layer type
          ::glBindTexture(GL_TEXTURE_2D, hirez ? textures_[i].texture : textures_[i].thumbnail);
          ::glEnable(GL_TEXTURE_2D);
        }
        else {                  //  color layer type
          ::glDisable(GL_TEXTURE_2D);
          if (l->blend() != 0) {  //  if not inactive, replace the constant color, and replace the texture args with constant args
            float color[4];
            color[0] = (l->color() & 0xff) / 255.0f;
            color[1] = ((l->color()>>8) & 0xff) / 255.0f;
            color[2] = ((l->color()>>16) & 0xff) / 255.0f;
            color[3] = ((l->color()>>24) & 0xff) / 255.0f;
            glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_CONSTANT);
            glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_CONSTANT);
          }
        }
      }
    }
    
    GLuint list_;
    IScene *scene_;
    int id_;
    std::vector<TextureInts> textures_;
};

int showCount = 0;

#pragma warning (disable: 4267 4244)

class WalkWindow : public WalkWindowBase {
  public:
    WalkWindow() {
      playerVel_ = Float3(0, 0, 0);
      playerPos_ = Float3(0, 0, 0);
      camPos_ = Float3(0, settings.height + settings.radius, 0);
      lastUpdate_ = 0;
      trimeshData_ = NULL;
      trimesh_ = NULL;
      capsule_ = NULL;
      ray_ = NULL;
      curWalkWindow_ = this;
      inhibitDelete_ = false;
      for (int fmt = 0; fmt < 2; ++fmt) {
        wnd_ = ::CreateWindowEx(
            WS_EX_APPWINDOW | WS_EX_LEFT,
            (LPCSTR)glClass,
            "Level Walker",
            WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
            10, 40,
            1024, 640,
            0,
            0,
            hModule,
            static_cast<WalkWindowBase *>(this));
        ::SetTimer(wnd_, 1, 10, 0);
        ignoreMove_ = true;
        POINT pt = { 1024/2, 640/2 };
        ::ClientToScreen(wnd_, &pt);
        ::SetCursorPos(pt.x, pt.y);
        dc_ = GetDC(wnd_);
        PIXELFORMATDESCRIPTOR pfd = {
          sizeof(PIXELFORMATDESCRIPTOR),   // size of this pfd 
          1,                     // version number 
          PFD_DRAW_TO_WINDOW |   // support window 
          PFD_SUPPORT_OPENGL |   // support OpenGL 
          PFD_DOUBLEBUFFER,      // double buffered 
          PFD_TYPE_RGBA,         // RGBA type 
          24,                    // 24-bit color depth 
          0, 0, 0, 0, 0, 0,      // color bits ignored 
          8,                     // 8-bit alpha buffer 
          0,                     // shift bit ignored 
          0,                     // no accumulation buffer 
          0, 0, 0, 0,            // accum bits ignored 
          24,                    // 24-bit z-buffer 
          8,                     // 8-bit stencil buffer 
          0,                     // no auxiliary buffer 
          PFD_MAIN_PLANE,        // main layer 
          0,                     // reserved 
          0, 0, 0                // layer masks ignored 
        };
        PIXELFORMATDESCRIPTOR pfd2 = pfd;
        pfd2.cAlphaBits = 0;
        pfd2.cColorBits = 16;
        pfd2.cStencilBits = 0;
        pfd2.cDepthBits = 16;
        int fmtix = ::ChoosePixelFormat(dc_, fmt ? &pfd2 : &pfd);
        ::SetPixelFormat(dc_, fmtix, fmt ? &pfd2 : &pfd);
        rc_ = wglCreateContext(dc_);
        ::wglMakeCurrent(dc_, rc_);
        char const *version = (char const *)glGetString(GL_VERSION);
        char const *dot = strchr(version, '.');
        if (dot)
          --dot;
        else
          dot = version;
        if (strcmp(dot, "1.3") < 0) {
          if (fmt == 0) {
            //  If this was the first time through, try again with a 
            //  less demanding pixel format. DeleD (as of version 1.71) 
            //  creates 16-bit formats, and some cards (ATI) can't have 
            //  both 16-bit and 24-bit open at the same time.
            ::wglMakeCurrent(0, 0);
            ::wglDeleteContext(rc_);
            inhibitDelete_ = true;
            ::DestroyWindow(wnd_);
            continue;
          }
          //  I'm toast!
          //  Put this error message on the stack.
          std::string str = std::string("The version of OpenGL installed on this computer is too old.\n")
              + "Installed version: " + version + "\nRequires version 1.3 or more.\n"
              + (char const *)glGetString(GL_VENDOR) + " " + (char const *)glGetString(GL_RENDERER);
          char volatile stackBuf[256];
          strncpy((char *)stackBuf, str.c_str(), 256);
          stackBuf[255] = 0;
          ::MessageBox(wnd_, str.c_str(), "OpenGL 1.3 required", MB_OK);
          delete this;
          //  throw a structured exception
          *(volatile char const *)1;
        }
        else {
          break;
        }
      }
      loadExtensionFunc("glActiveTexture", (void**)&glActiveTexture);
      loadExtensionFunc("glMultiTexCoord2f", (void**)&glMultiTexCoord2f);
      if (strstr((char const *)glGetString(GL_EXTENSIONS), "GL_ARB_occlusion_query")) {
        loadExtensionFunc("glGenQueries", (void**)&glGenQueriesARB);
        loadExtensionFunc("glDeleteQueries", (void**)&glDeleteQueriesARB);
        loadExtensionFunc("glBeginQuery", (void**)&glBeginQueryARB);
        loadExtensionFunc("glEndQuery", (void**)&glEndQueryARB);
        loadExtensionFunc("glGetQueryObjectuiv", (void**)&glGetQueryObjectuivARB);
      }
    }

    bool InhibitDelete() {
      if (inhibitDelete_) {
        inhibitDelete_ = false;
        return true;
      }
      return false;
    }

    void loadExtensionFunc(char const *function, void**oFunc) {
      *oFunc = (void *)::wglGetProcAddress(function);
      if (!*oFunc) {
        std::string s(function);
        s += "ARB";
        *oFunc = (void *)::wglGetProcAddress(function);
      }
      if (!*oFunc) {
        std::string s(function);
        s += "EXT";
        *oFunc = (void *)::wglGetProcAddress(function);
      }
      if (!*oFunc) {
        std::string s("The OpenGL implementation on this computer does not expose the function ");
        s += function;
        s += ".";
        ::MessageBox(wnd_, s.c_str(), "OpenGL error", MB_OK);
      }
    }

    ~WalkWindow() {
      if (trimesh_) {
        dGeomTriMeshDataDestroy(trimeshData_);
        dGeomDestroy(trimesh_);
        dGeomDestroy(capsule_);
        dGeomDestroy(ray_);
      }
      std::map<std::string, GLuint>().swap(textureCache_);
      std::map<std::string, GLuint>().swap(smallTextureCache_);
      assert(curWalkWindow_ == this);
      curWalkWindow_ = 0;
      ::SetWindowLong(wnd_, GWL_USERDATA, 0);
      for (std::map<int, Material *>::iterator ptr = materials_.begin(), end = materials_.end();
          ptr != end; ++ptr) {
        delete (*ptr).second;
      }
      std::map<int, Material *>().swap(materials_);
      if (rc_) {
        wglMakeCurrent(0, 0);
        wglDeleteContext(rc_);
        rc_ = 0;
        dc_ = 0;
      }
      if (wnd_) {
        ::DestroyWindow(wnd_);
        wnd_ = 0;
      }
      while (showCount < 0) {
        ::ShowCursor(true);
        ++showCount;
      }
    }
    
    bool inhibitDelete_;
    AutoDispose<IScene> scene_;

    void Init(AutoDispose<IScene> &scene) {
      scene_ = scene.detach();
      leftDown_ = false;
      upDown_ = false;
      rightDown_ = false;
      downDown_ = false;
      elevateDown_ = false;
      descendDown_ = false;
      jumpDown_ = false;
      yaw_ = 0;
      pitch_ = 0;
      ignoreMove_ = false;
      gravity_ = false;
      groundContact_ = 0;
      propulsion_ = false;
    }

    bool leftDown_;
    bool upDown_;
    bool rightDown_;
    bool downDown_;
    bool elevateDown_;
    bool descendDown_;
    bool jumpDown_;
    bool gravity_;
    bool propulsion_;
    int groundContact_;
    bool ignoreMove_;
    float yaw_;
    float pitch_;
    Float3 camPos_;
    Float3 playerPos_;
    Float3 playerVel_;

    void MovementKey(WPARAM wParam, bool state) {
      switch (wParam) {
        case 'A':
          leftDown_ = state;
          break;
        case 'W':
          upDown_ = state;
          break;
        case 'D':
          rightDown_ = state;
          break;
        case 'S':
          downDown_ = state;
          break;
        case 'E':
          elevateDown_ = state;
          break;
        case 'C':
          descendDown_ = state;
          break;
        case ' ':
          jumpDown_ = state;
          break;
        case 'G':
          if (state)
            gravity_ = true;
          break;
        case 'F':
          if (state)
            gravity_ = false;
          break;
        case 'T':
          if (state)
            hirez_ = !hirez_;
          break;
      }
    }
    void DoKeyDown(WPARAM wParam, LPARAM lParam) {
      switch (wParam) {
        case VK_ESCAPE:
          ::PostMessage(wnd_, WM_CLOSE, 0, 0);
          break;
        default:
          MovementKey(wParam, true);
          break;
      }
    }
    void DoKeyUp(WPARAM wParam, LPARAM lParam) {
      MovementKey(wParam, false);
    }
    void DoMouseDown(WPARAM wParam, LPARAM lParam) {
    }
    void DoMouseUp(WPARAM wParam, LPARAM lParam) {
    }
    void DoMouseMove(WPARAM wParam, LPARAM lParam) {
      if (!ignoreMove_ && showCount < 0) {
        RECT rt;
        ::GetClientRect(wnd_, &rt);
        POINT pt = { rt.right/2, rt.bottom/2 };
        POINT pt2 = { (lParam&0xffff), ((lParam>>16)&0xffff) };
        if (pt2.x != pt.x || pt2.y != pt.y) {
#if !defined(NDEBUG)
          char buf[400];
          sprintf(buf, "move delta %d,%d - %d,%d = %d,%d\n", pt2.x, pt2.y, pt.x, pt.y, pt2.x-pt.x, pt2.y-pt.y);
          ::OutputDebugString(buf);
#endif
          yaw_ += ((int)pt2.x - (int)pt.x) * 0.1f;
          while (yaw_ < -180) {
            yaw_ += 360;
          }
          while (yaw_ > 180) {
            yaw_ -= 360;
          }
          pitch_ += ((int)pt2.y - (int)pt.y) * 0.1f;
          if (pitch_ < -80) pitch_ = -80;
          if (pitch_ > 80) pitch_ = 80;
          ::ClientToScreen(wnd_, &pt);
          ignoreMove_ = true;
          ::SetCursorPos(pt.x, pt.y);
        }
      }
      else {
        ignoreMove_ = false;
      }
    }
    void DoPaint(WPARAM wParam, LPARAM lParam) {
      PAINTSTRUCT ps;
      ::BeginPaint(wnd_, &ps);
      ::EndPaint(wnd_, &ps);
      DrawScene();
      assert(!glGetError());
    }
    void DoActivate(WPARAM wParam, LPARAM lParam) {
      if ((wParam&0xffff) == WA_INACTIVE) {
        if (showCount < 0) {
          ::ShowCursor(true);
          ++showCount;
        }
      }
      else {
        if (showCount >= 0) {
          ::ShowCursor(false);
          --showCount;
        }
      }
    }
    void DoTimer(WPARAM wParam, LPARAM lParam) {
      if (showCount < 0) {
        UpdateScene();
        DrawScene();
        assert(!glGetError());
      }
    }

    double lastUpdate_;

    int MergeContacts(dContactGeom *contacts, int n) {
      for (int i = 0; i < n; ++i) {
        for (int j = i+1; j < n; ++j) {
          if (length((Float3 const &)*contacts[i].pos - (Float3 const &)*contacts[j].pos) < 0.01) {
            (Float3 &)*contacts[i].normal += (Float3 const &)*contacts[j].normal;
            normalize((Float3 &)*contacts[i].normal);
            contacts[i].depth += contacts[j].depth;
            contacts[i].depth *= 0.5f;
            contacts[j] = contacts[n-1];
            --n;
            --j;
          }
        }
      }
      return n;
    }

    #define FORCE (settings.height / 5)
    #define MAX_SPEED (settings.height * 6)

    void UpdateScene() {
      //  time-dependent update
      double now = ReadTimer();
      float dt = (float)(now - lastUpdate_);
      if (dt > 0.1f) dt = 0.1f;
      lastUpdate_ = now;
      //  movement
      float dz = -::cosf(float(yaw_ * M_PI / 180));
      float dx = ::sinf(float(yaw_ * M_PI / 180));
      propulsion_ = false;
      if (upDown_) {
        propulsion_ = true;
        if (groundContact_) {
          playerVel_.x_ += dx*FORCE*settings.height*dt;
          playerVel_.z_ += dz*FORCE*settings.height*dt;
        }
      }
      if (downDown_) {
        propulsion_ = true;
        if (groundContact_) {
          playerVel_.x_ -= dx*FORCE*settings.height*dt;
          playerVel_.z_ -= dz*FORCE*settings.height*dt;
        }
      }
      if (rightDown_) {
        propulsion_ = true;
        if (groundContact_) {
          playerVel_.x_ -= dz*FORCE*settings.height*dt;
          playerVel_.z_ += dx*FORCE*settings.height*dt;
        }
      }
      if (leftDown_) {
        propulsion_ = true;
        if (groundContact_) {
          playerVel_.x_ += dz*FORCE*settings.height*dt;
          playerVel_.z_ -= dx*FORCE*settings.height*dt;
        }
      }
      if (elevateDown_) {
        propulsion_ = true;
        playerVel_.y_ += FORCE*settings.height*dt;
      }
      if (descendDown_) {
        propulsion_ = true;
        playerVel_.y_ -= FORCE*settings.height*dt;
      }
      if (jumpDown_) {
        propulsion_ = true;
        jumpDown_ = false;
        if (groundContact_) {
          playerVel_.y_ += FORCE*settings.height*0.25f;
        }
      }
      //  collision and physics
      if (gravity_) {
        playerVel_.y_ -= FORCE*settings.height*dt*0.5f;
      }
      //  trace ray from old campos to new campos
      //  if intersected, then stop
      Float3 newCamPos = camPos_;
      newCamPos += (playerVel_ * dt);
      Float3 dir = (newCamPos - camPos_);
      float len = length(dir);
      dir *= 1.0f/len;
      dGeomRaySet(ray_, camPos_.x_, camPos_.y_, camPos_.z_, dir.x_, dir.y_, dir.z_);
      dGeomRaySetParams(ray_, 0, 0);
      dGeomRaySetLength(ray_, len);
      dGeomRaySetClosestHit(ray_, 1);
      dContactGeom contacts[20];
      bool stopped = false;
      if (gravity_ && trimesh_) {
        int n = dCollide(ray_, trimesh_, 20, contacts, sizeof(contacts[0]));
        if (n > 0) {
          playerVel_ = Float3(0, 0, 0);
          //  todo: play "ouch" sound
          stopped = true;
        }
      }
      //  travel
      if (groundContact_ > 0)
        --groundContact_;
      if (!stopped) {
        Float3 newPlayerPos = playerPos_ + (playerVel_ * dt);
        //  run capsule collision
        if (gravity_ && trimesh_) {
          //  Use a ray for "legs" to allow walking over rough terrain and 
          //  stairsteps.
          dGeomRaySetLength(ray_, settings.radius);
          dGeomRaySet(ray_, newPlayerPos.x_, newPlayerPos.y_ + settings.radius, newPlayerPos.z_, 0, -1, 0);
          int base = dCollide(ray_, trimesh_, 20, contacts, sizeof(contacts[0]));
          if (base) {
            float depth = length((Float3 &)contacts[0].pos[0] - newPlayerPos);
            float desiredY = depth * 5; //  solve this penetration in 1/5 of a second
            if (playerVel_.y_ < desiredY) {
              playerVel_.y_ = desiredY;
            }
            groundContact_ = 5;
          }
          dMatrix3 R = { 1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0 };
          dGeomSetRotation(capsule_, R);
          dGeomSetPosition(capsule_, 
              newPlayerPos.x_, newPlayerPos.y_ + settings.radius * 1.5f + settings.height*0.5f, newPlayerPos.z_);
          int n = dCollide(capsule_, trimesh_, 20, contacts, sizeof(contacts[0]));
          n = MergeContacts(contacts, n);
          //  resolve penetration -- this is a cheesy way
          Float3 pen = Float3(0, 0, 0);
          float dx[2] = {0,0}, dy[2] = {0,0}, dz[2] = {0,0};
          for (int i = 0; i < n; ++i) {
            if (contacts[i].normal[0] > 0) dx[0] = std::max(dx[0], contacts[i].normal[0]*contacts[i].depth);
            if (contacts[i].normal[1] > 0) dy[0] = std::max(dy[0], contacts[i].normal[1]*contacts[i].depth);
            if (contacts[i].normal[2] > 0) dz[0] = std::max(dz[0], contacts[i].normal[2]*contacts[i].depth);
            if (contacts[i].normal[0] < 0) dx[1] = std::min(dx[1], contacts[i].normal[0]*contacts[i].depth);
            if (contacts[i].normal[1] < 0) dy[1] = std::min(dy[1], contacts[i].normal[1]*contacts[i].depth);
            if (contacts[i].normal[2] < 0) dz[1] = std::min(dz[1], contacts[i].normal[2]*contacts[i].depth);
          }
          //  See if we have counter-acting forces.
          //  Currently disabled, as it would wedge in concave triangle fans 
          //  on the ground.
          {
            if (dx[0] != 0 || dx[1] != 0 || dy[0] != 0 || dy[1] != 0 || dz[0] != 0 || dz[1] != 0) {
              playerVel_ += Float3(dx[0] + dx[1], dy[0] + dy[1], dz[0] + dz[1]) * (1.0f / dt);
            }
            if (dy[0] > 0) {
              groundContact_ = 5;
            }
          }
        }
        else {
          groundContact_ = true;  //  cheat
        }
      }
      float pvLen = length(playerVel_);
      if (pvLen > MAX_SPEED) {
        playerVel_ *= (MAX_SPEED / pvLen);
      }
      if (!stopped) {
        playerPos_ += (playerVel_ * dt);
        float scale = powf((propulsion_ || !groundContact_) ? 0.75f : 0.04f, dt);
        playerVel_ *= scale;
        if (gravity_) {
          Float3 pvxz = playerVel_;
          pvxz.y_ = 0;
          if (length(pvxz) < settings.height/8 && !propulsion_) {
            playerVel_.x_ = playerVel_.z_ = 0;
          }
        }
        else {
          if (length(playerVel_) < settings.height / 8 && !propulsion_) {
            playerVel_ = Float3(0,0,0);
          }
        }
      }
      camPos_ = playerPos_ + Float3(0, settings.height + settings.radius, 0);
      if (stopped) {
        leftDown_ = false;
        rightDown_ = false;
        upDown_ = false;
        downDown_ = false;
        elevateDown_ = false;
        descendDown_ = false;
      }
    }

    std::vector<GLuint> queries_;
    std::vector<GLuint> lists_;
    std::vector<Float3> minimums_;
    std::vector<Float3> maximums_;
    std::vector<int> materialsPerList_;
    std::vector<Float3> savedVerts_;
    std::vector<int> savedIndices_;
    dTriMeshDataID trimeshData_;
    dGeomID trimesh_;
    dGeomID capsule_;
    dGeomID ray_;

    static Float3 camera_aabb_vector(Float3 const &minimum, Float3 const &maximum, Float3 const &camera) {
      //  calculate direction FROM camera TO box
      Float3 d(0, 0, 0);
      if (camera.x_ < minimum.x_)
        d.x_ = minimum.x_ - camera.x_;
      else if (camera.x_ > maximum.x_)
        d.x_ = maximum.x_ - camera.x_;
      if (camera.y_ < minimum.y_)
        d.y_ = minimum.y_ - camera.y_;
      else if (camera.y_ > maximum.y_)
        d.x_ = maximum.y_ - camera.y_;
      if (camera.z_ < minimum.z_)
        d.z_ = minimum.z_ - camera.z_;
      else if (camera.z_ > maximum.z_)
        d.z_ = maximum.z_ - camera.z_;
      return d;
    }
    static float aabb_camera_distance_squared(Float3 const &minimum, Float3 const &maximum, Float3 const &camera) {
      return length_squared(camera_aabb_vector(minimum, maximum, camera));
    }

    void DrawScene() {
      //  might not have gotten the scene yet (fix for ATI drivers)
      if (!scene_)
        return;
      //  set up window
      ::wglMakeCurrent(dc_, rc_);
      RECT cr;
      ::GetClientRect(wnd_, &cr);
      ::glViewport(0, 0, cr.right, cr.bottom);
      ::glClearColor(0.3f, 0.3f, 0.3f, 0.0f);
      ::glClearDepth(1.0f);
      ::glClearStencil(0);
      ::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
      ::glDisable(GL_LIGHTING);

      ::glEnable(GL_CULL_FACE);
      ::glFrontFace(GL_CW);
      ::glCullFace(GL_BACK);
      ::glEnable(GL_DEPTH_TEST);
      ::glDepthFunc(GL_LEQUAL);

      //  set up camera
      float fovx, fovy;
      if (cr.right > cr.bottom) { //  common case
        fovy = 40;
        //  D * sin y = bottom
        //  D = bottom / sin y
        //  D * sin ? = right
        //  sin ? = right * sin y / bottom
        fovx = asinf(::sinf(fovy * M_PI / 180) * cr.right / cr.bottom);
      }
      else {
        fovx = 40;
        fovy = asinf(::sinf(fovx * M_PI / 180) * cr.bottom / cr.right) * 180 / M_PI;
      }
      ::glMatrixMode(GL_PROJECTION);
      ::glLoadIdentity();
      ::gluPerspective(fovy, (float)cr.right/(float)cr.bottom, 1.0f, 100000.0f);
      ::glMatrixMode(GL_MODELVIEW);
      ::glLoadIdentity();
      //  camera translation and rotation
      glRotatef(pitch_, 1, 0, 0);
      glRotatef(yaw_, 0, 1, 0);
      glTranslatef(-camPos_.x_, -camPos_.y_, -camPos_.z_);

      Float3 frustumPoints[6];
      float frustumDistances[6];
      get_camera_frustum(frustumPoints, frustumDistances);

      //  cache the whole thing to lists
      if (lists_.size()) {
        std::vector<std::pair<float, int> > distances;
        //  todo: handle transparent materials in a separate list
        for (size_t j = 0; j < lists_.size(); ++j) {
          //  todo: frustum cull this list based on the AABB
          if (aabb_frustum_visible(minimums_[j], maximums_[j], frustumPoints, frustumDistances))
            distances.push_back(std::pair<float, int>(
                aabb_camera_distance_squared(minimums_[j], maximums_[j], camPos_), j));
        }
        std::sort(distances.begin(), distances.end());
        for (size_t j = 0; j < distances.size(); ++j) {
          size_t i = distances[j].second;
          Material volatile *m = GetMaterial(materialsPerList_[i]);
          if (glGenQueriesARB != 0) {
            GLuint ui;
            glGetQueryObjectuivARB(queries_[i], GL_QUERY_RESULT_ARB, &ui);
            glBeginQueryARB(GL_SAMPLES_PASSED_ARB, queries_[i]);
            //  Only use a high-resolution material if a noticeable part of the 
            //  geometry was visible last time around.
            const_cast<Material *>(m)->Apply(ui > 10);
            glCallList(lists_[i]);
            glEndQueryARB(GL_SAMPLES_PASSED_ARB);
          }
          else {
            const_cast<Material *>(m)->Apply(true);
            glCallList(lists_[i]);
          }
        }
      }
      else {
        //  build each piece of the scene
        std::map<Float3, int> colVerts;
        std::vector<int> colIndices;
        std::map<Float3, int>::iterator end = colVerts.end();
        int ix[3];  //  indices of last triangle in fan
        //  Traverse once per material -- not super efficient, but it's only 
        //  done on the first cycle through.
        for (size_t mi = 0, mn = scene_->materials()->count(); mi != mn; ++mi) {
          bool set = false;
          GLuint list = 0;
          IInterface<IObject *> *objects = scene_.get()->objects();
          size_t n_objects = objects->count();
          Float3 minimum(1e6,1e6,1e6);
          Float3 maximum(-1e6,-1e6,-1e6);
          for (size_t i = 0; i < n_objects; ++i) {
            IObject *obj = objects->item(i);
            IInterface<Float3> *vertices = obj->vertices();
            IInterface<Poly> *polygons = obj->polygons();
            size_t n_polygons = polygons->count();
            for (size_t j = 0; j < n_polygons; ++j) {
              Poly const &p = polygons->item(j);
              if (p.material_ == mi) {
                if (!set) {
                  set = true;
                  //  set up material
                  Material *m = GetMaterial(mi);
                  //  pre-load only the small textures
                  m->Apply(false);
                  list = glGenLists(1);
                  glNewList(list, GL_COMPILE);
                  //  todo: figure out whether this material is collidable or not
                }
                unsigned int n_coords = p.numCoords();
                size_t n_indices = p.indices().size();
                ::glBegin(GL_TRIANGLE_FAN);
                for (size_t k = 0; k < n_indices; ++k) {
                  //  texture coordinates
                  for (unsigned int l = 0; l < n_coords; ++l) {
                    glMultiTexCoord2f(GL_TEXTURE0 + l, p.coords(l)[k].u_, p.coords(l)[k].v_);
                  }
                  //  build collision tris
                  int q = k;
                  if (q > 2) {
                    ix[1] = ix[2];
                    q = 2;
                  }
                  Float3 v = vertices->item(p.indices()[k]);
                  std::map<Float3, int>::iterator ptr = colVerts.find(v);
                  int zix = colVerts.size();
                  if (ptr == end) {
                    colVerts[v] = zix;
                  }
                  else {
                    zix = (*ptr).second;
                  }
                  ix[q] = zix;
                  if (q == 2) {
                    colIndices.push_back(ix[0]);
                    colIndices.push_back(ix[2]);
                    colIndices.push_back(ix[1]);
                  }
                  //  vertex coordinates
                  ::glVertex3fv(&v.x_);
                  minimize(minimum, v);
                  maximize(maximum, v);
                }
                ::glEnd();
              } //  if() material
            } //  for() polygons
          } //  for() objects
          if (set) {
            glEndList();
            lists_.push_back(list);
            minimums_.push_back(minimum);
            maximums_.push_back(maximum);
            materialsPerList_.push_back(mi);
            glCallList(list);
          }
        } //  for() material
        //  build collision
        savedVerts_.resize(colVerts.size());
        Float3 maximum(0,0,0), minimum(0,0,0);
        for (std::map<Float3, int>::iterator ptr = colVerts.begin(), end = colVerts.end();
            ptr != end; ++ptr) {
          savedVerts_[(*ptr).second] = (*ptr).first;
          maximize(maximum, (*ptr).first);
          minimize(minimum, (*ptr).first);
        }
        //  set the player at the center, at the top
        playerPos_ = (minimum + maximum) * 0.5f;
        playerPos_.y_ = maximum.y_;
        camPos_ = playerPos_ + Float3(0, settings.height + settings.radius, 0);
        savedIndices_.swap(colIndices);
        if (savedVerts_.size() && savedIndices_.size()) {
          trimeshData_ = dGeomTriMeshDataCreate();
          dGeomTriMeshDataBuildSingle(trimeshData_, &savedVerts_[0], 12, savedVerts_.size(), 
              &savedIndices_[0], savedIndices_.size(), 12);
          trimesh_ = dCreateTriMesh(0, trimeshData_, 0, 0, 0);
        }
        else {
          trimesh_ = NULL;
        }
        float adjustedHeight = settings.height - settings.radius;
        if (adjustedHeight < 0)
          adjustedHeight = 0;
        capsule_ = dCreateCapsule(0, settings.radius, adjustedHeight);
        ray_ = dCreateRay(0, settings.height*10);
        if (glGenQueriesARB != 0 && lists_.size() > 0) {
          queries_.resize(lists_.size());
          glGenQueriesARB(lists_.size(), &queries_[0]);
          for (size_t i = 0, n = queries_.size(); i != n; ++i) {
            glBeginQueryARB(GL_SAMPLES_PASSED_ARB, queries_[i]);
            glEndQueryARB(GL_SAMPLES_PASSED_ARB);
          }
        }
      } //  if() lists built

      //  display!
      ::SwapBuffers(dc_);
    }

    Material *GetMaterial(int mat) {
      std::map<int, Material *>::iterator ptr = materials_.find(mat);
      if (ptr == materials_.end()) {
        Material *m = new Material(scene_.get(), mat);
        materials_[mat] = m;
        return m;
      }
      return (*ptr).second;
    }

    std::map<int, Material *> materials_;    
    HWND wnd_;
    HDC dc_;
    HGLRC rc_;
};

//  WalkScene extracts the data from the scene, converts it to 
//  ODE collision and GL geometry formats, and plunks down the player 
//  in the scene. It's un-clear where to start putting the player, 
//  though. Perhaps a raycast at the origin would be a good first bet.
static void WalkScene(AutoDispose<IScene> &scene)
{
  if (curWalkWindow_) {
    delete curWalkWindow_;
  }
  WalkWindow *ww = new WalkWindow();
  ww->Init(scene);
}




static TCallback gCallback;

extern "C" {

PCHAR DLL_EXPORT __stdcall PluginName()
{
  return (PCHAR)"LevelWalker Plugin";
}

PCHAR DLL_EXPORT __stdcall PluginDescription()
{
  return (PCHAR)"Walk through a level using FPS controls.";
}

PCHAR DLL_EXPORT __stdcall PluginDeleDVersion()
{
  return (PCHAR)"1.6";
}

PCHAR DLL_EXPORT __stdcall PluginVersion()
{
  return (PCHAR)"1.0";
}

PCHAR DLL_EXPORT __stdcall PluginAuthor()
{
  return (PCHAR)"Jon Watte";
}

PCHAR DLL_EXPORT __stdcall PluginEmail()
{
  return (PCHAR)"deled-levelwalker@mindcontrol.org";
}

static LRESULT CALLBACK MessageProc(int, WPARAM, LPARAM);
static MODULEINFO gModuleInformation;

#pragma warning(disable: 4311 4312)

bool crashed = false;

int CrashFunc(_EXCEPTION_POINTERS *ptrs) {
  if (!crashed) {
    //  don't write more than one dump
    crashed = true;

    char const *str = ".\\levelwalker.mdmp";
    char name[512+32];
    if (::GetModuleFileName(::GetModuleHandle(0), name, 512) < 511) {
      char * s = strrchr(name, '\\');
      if (s) {
        strcpy(s+1, "levelwalker.mdmp");
        str = name;
      }
    }

    HANDLE hFile = ::CreateFile(
        str,
        GENERIC_WRITE | STANDARD_RIGHTS_WRITE,
        FILE_SHARE_READ,
        0,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        0);
    MINIDUMP_EXCEPTION_INFORMATION mei;
    mei.ClientPointers = false;
    mei.ExceptionPointers = ptrs;
    mei.ThreadId = ::GetCurrentThreadId();
    if ((hFile != INVALID_HANDLE_VALUE) && ::MiniDumpWriteDump(
        ::GetCurrentProcess(),
        ::GetCurrentProcessId(),
        hFile,
        MiniDumpWithIndirectlyReferencedMemory,
        &mei,
        0,
        0)) {
      ::CloseHandle(hFile);
      ::MessageBox(0, "Levelwalker encountered an internal error.\n"
          "A file called 'levelwalker.mdmp' has been created.\n"
          "Please post an issue on levelwalker.googlecode.com\n"
          "and attach that file for debugging. Also mention\n"
          "the build date: " STRINGIZE(__DATE__) " " STRINGIZE(__TIME__) ".",
          "Levelwalker Error", MB_OK);
    }
    else {
      ::MessageBox(0, "Levelwalker has encountered an internal error.\n"
          "Unfortunately, a minidump file could not be created.\n"
          "Please report this problem on levelwalker.googlecode.com.\n"
          "Build date: " STRINGIZE(__DATE__) " " STRINGIZE(__TIME__) ".",
          "Levelwalker Error", MB_OK);
    }
  }
  return EXCEPTION_EXECUTE_HANDLER;
}

static LRESULT CALLBACK GlWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  __try {
    WalkWindowBase *wwb = (WalkWindowBase *)::GetWindowLong(hWnd, GWL_USERDATA);
    if (msg == WM_NCCREATE) {
      CREATESTRUCT *cs = (CREATESTRUCT *)lParam;
      if (cs->lpCreateParams) {
        ::SetWindowLong(hWnd, GWL_USERDATA, (LONG)cs->lpCreateParams);
      }
    }
    if (wwb) {
      switch (msg) {
        //  whichever comes first
        case WM_CLOSE:
        case WM_DESTROY:
          if (!wwb->InhibitDelete())
            delete wwb;
          break;
        case WM_KEYDOWN:
          wwb->DoKeyDown(wParam, lParam);
          break;
        case WM_KEYUP:
          wwb->DoKeyUp(wParam, lParam);
          break;
        case WM_LBUTTONDOWN:
          wwb->DoMouseDown(wParam, lParam);
          break;
        case WM_LBUTTONUP:
          wwb->DoMouseUp(wParam, lParam);
          break;
        case WM_MOUSEMOVE:
          wwb->DoMouseMove(wParam, lParam);
          break;
        case WM_PAINT:
          wwb->DoPaint(wParam, lParam);
          break;
        case WM_ACTIVATE:
          wwb->DoActivate(wParam, lParam);
          break;
        case WM_TIMER:
          wwb->DoTimer(wParam, lParam);
          break;
      }
    }
  }
  __except(CrashFunc(GetExceptionInformation())) {
    //  I don't want this window any more! It's mis-beahving!
    ::SetWindowLong(hWnd, GWL_USERDATA, 0);
    ::DestroyWindow(hWnd);
  }
  return DefWindowProc(hWnd, msg, wParam, lParam);
}

static void RegisterGlClass()
{
  WNDCLASSEX wcex;
  memset(&wcex, 0, sizeof(wcex));
  wcex.cbSize = sizeof(wcex);
  wcex.hInstance = hModule;
  wcex.lpfnWndProc = GlWndProc;
  wcex.lpszClassName = "levelwalker GL window";
  wcex.style = CS_OWNDC;
  glClass = ::RegisterClassEx(&wcex);

  ilInit();
  iluInit();
  ilutRenderer(ILUT_OPENGL);
}

void DLL_EXPORT __stdcall PluginSetCallback(void * ptr)
{
  if (gCallback == NULL) {
    ::InitCommonControls();
  }
  //  When starting up, capture state of host application
  gCallback = TCallback(ptr);

  if (hModule == NULL) {
    hModule = ::GetModuleHandle("levelwalker.dll"); //  hope nobody renamed me!
  }
  if (hModule == NULL) {
    ::MessageBox(0, 
        "LevelWalker can't find the dialog resources!\nLevelWalker won't work correctly.", 
        "LevelWalker", MB_OK);
    return;
  }

  if (!glClass) {
    RegisterGlClass();
  }
}


static void DoWalk()
{
  try {
    AutoDispose<IScene> scene(NewScene(gCallback));
    WalkScene(scene);
  }
  catch(...) {
    ::MessageBox(0, 
        "Unknown exception while setting up scene.\nSorry this message can't be more helpful.", 
        "LevelWalker Error", MB_OK);
  }
}

static bool SettingsOK(bool kvetch = true)
{
  if (settings.radius < 1) {
    if (kvetch) {
      ::MessageBox(0, (std::string("The radius is too small.\n")).c_str(),
          "LevelWalker", MB_OK);
    }
    return false;
  }
  if (settings.height < 1) {
    if (kvetch) {
      ::MessageBox(0, "The height is too small.", "LevelWalker", MB_OK);
    }
    return false;
  }
  if (settings.radius > settings.height / 2) {
    if (kvetch) {
      ::MessageBox(0, "Radius can't be bigger than half height.", "LevelWalker", MB_OK);
    }
    return false;
  }
  return true;
}

static void UpdateDerivedControls()
{
}

static void ReadSettings()
{
  std::string fn;
  GetSettingsFilename(fn);
  FILE * f = fopen(fn.c_str(), "rb");
  if (f != NULL) {
    fseek(f, 0, 2);
    long l = ftell(f);
    fseek(f, 0, 0);
    char * buf = new char[l+1];
    fread(buf, 1, l, f);
    buf[l] = 0;
    fclose(f);

    class SS {
      public:
        char const * str_;
        SS(char const * str) : str_(str) {}
        void doBool(int id, char const * name, bool & b) {
          b = getarg(name) == "true";
        }
        void doInt(int id, char const *name, int & i) {
          i = atoi(getarg(name).c_str());
        }
        void doString(int id, char const * name, std::string & str) {
          str = getarg(name);
        }
        void doRatio(int id, char const * name, float & numer, float & denom) {
          numer = 1;
          denom = 1;
          sscanf(getarg(name).c_str(), " %f / %f", &numer, &denom);
        }
        void doRadio(int const * ids, char const * name, int & value) {
          value = atoi(getarg(name).c_str());
        }
        std::string getarg(char const * name) {
          std::string fnd = std::string("\n") + name + "=";
          char const * pos = strstr(str_, fnd.c_str());
          if (pos) {
            pos += fnd.length();
            char const * end = strchr(pos, '\n');
            return std::string(pos, end ? end-pos : strlen(pos));
          }
          return std::string("");
        }
    } ss(buf);
    VisitSettings(settings, ss);
    
    delete[] buf;
    settingsDirty = false;
 }
}

static void ApplySettings()
{
  class {
    public:
      void doBool(int id, char const * name, bool & b) {
        ::CheckDlgButton(hDialog, id, b ? BST_CHECKED : BST_UNCHECKED);
      }
      void doInt(int id, char const *name, int & i) {
        ::SetDlgItemInt(hDialog, id, i, false);
      }
      void doString(int id, char const * name, std::string & str) {
        ::SetDlgItemText(hDialog, id, str.c_str());
      }
      void doRatio(int id, char const * name, float & numer, float & denom) {
        char buf[100];
        if (denom != 1) {
          sprintf(buf, "%g/%g", numer, denom);
        }
        else {
          sprintf(buf, "%.3f", numer);
        }
        ::SetDlgItemText(hDialog, id, buf);
      }
      void doRadio(int const * ids, char const * name, int & value) {
        for (int i = 0; ids[i]; ++i) {
          ::CheckDlgButton(hDialog, ids[i], value == i ? BST_CHECKED : BST_UNCHECKED);
        }
      }
  } ss;
  VisitSettings(settings, ss);
  settingsDirty = false;  //  I just set them!
}

static void ExtractSettings()
{
  class {
    public:
      void doBool(int id, char const * name, bool & b) {
        b = ::IsDlgButtonChecked(hDialog, id) ? true : false;
      }
      void doInt(int id, char const *name, int & i) {
        BOOL xl = false;
        i = ::GetDlgItemInt(hDialog, id, &xl, false);
      }
      void doString(int id, char const * name, std::string & str) {
        size_t s = 512;
        str.resize(s);
        s = ::GetDlgItemText(hDialog, id, &str[0], (int)s);
        str.resize(s);
      }
      void doRatio(int id, char const * name, float & numer, float & denom) {
        char buf[100];
        size_t s = ::GetDlgItemText(hDialog, id, buf, sizeof(buf));
        if (s == 100) s = 99;
        buf[s] = 0;
        int i = sscanf(buf, " %f / %f", &numer, &denom);
        if (i == 0) {
          numer = denom = 1;
        }
        else if (i == 1) {
          denom = 1;
        }
      }
      void doRadio(int const * ids, char const * name, int & value) {
        value = 0;
        for (int i = 0; ids[i]; ++i) {
          if (::IsDlgButtonChecked(hDialog, ids[i])) {
            value = i;
            break;
          }
        }
      }
  } ss;
  VisitSettings(settings, ss);
}

static void SaveSettings()
{
  std::string fn;
  GetSettingsFilename(fn);
  FILE * f = fopen(fn.c_str(), "wb");
  if (f) {
    fprintf(f, "version=1\n" );
    class SS {
      public:
        FILE * f_;
        SS(FILE * f) : f_(f) {}
        void doBool(int id, char const * name, bool & b) {
          fprintf(f_, "%s=%s\n", name, b ? "true" : "false");
        }
        void doInt(int id, char const *name, int & i) {
          fprintf(f_, "%s=%d\n", name, i);
        }
        void doString(int id, char const * name, std::string & str) {
          fprintf(f_, "%s=%s\n", name, str.c_str());
        }
        void doRatio(int id, char const * name, float & numer, float & denom) {
          if (denom == 1) {
            fprintf(f_, "%s=%.3f\n", name, numer);
          }
          else {
            fprintf(f_, "%s=%g/%g\n", name, numer, denom);
          }
        }
        void doRadio(int const * ids, char const * name, int & value) {
          fprintf(f_, "%s=%d\n", name, value);
        }
    } ss(f);
    VisitSettings(settings, ss);
    fclose(f);
    settingsDirty = false;
  }
  else {
#if !defined(NDEBUG)
    ::OutputDebugString("Could not save settings file for LevelWalker\n");
#endif
  }
}

static void DlgCommand(int cmd, HWND dlg, HWND ctl)
{
#if !defined(NDEBUG)
  char buf[256];
  sprintf(buf, "LevelWalker: DlgCommand %d 0x%lx 0x%lx\n", cmd, (size_t)dlg, (size_t)ctl);
  ::OutputDebugString(buf);
#endif

  bool dirtySettings = false;

  switch (cmd) {
    case IDCANCEL:
      delete curWalkWindow_;
      ::DestroyWindow(hDialog);
      break;
    case IDOK:
      if (settingsDirty) {
        ExtractSettings();
      }
      if (SettingsOK()) {
        if (settingsDirty) {
          SaveSettings();
        }
        ::DoWalk();
      }
      break;
    case IDC_RADIUS:
    case IDC_HEIGHT:
      dirtySettings = true;
      break;
  }
  if (dirtySettings) {
    UpdateDerivedControls();
    settingsDirty = true;
  }
}

static INT_PTR CALLBACK DlgProc(HWND dlg, UINT msg, WPARAM wparam, LPARAM lparam)
{
  INT_PTR ret = true;

  __try {

    switch (msg) {
      case WM_CLOSE:
        DlgCommand(IDCANCEL, dlg, ::GetDlgItem(dlg, IDCANCEL));
        break;
      case WM_COMMAND:
        DlgCommand((int)(wparam & 0xffff), dlg, (HWND)lparam);
        break;
      case WM_INITDIALOG:
#if !defined(NDEBUG)
        ::OutputDebugString("LevelWalker: WM_INITDIALOG()\n");
#endif
        hDialog = dlg;
        ReadSettings();
        ApplySettings();
        UpdateDerivedControls();
        break;
      case WM_DESTROY:
        hDialog = NULL;
#if !defined(NDEBUG)
        ::OutputDebugString("LevelWalker: unhooking GetMessage()\n");
#endif
        ::UnhookWindowsHookEx(hHook);
        hHook = NULL;
        break;
      default:
        ret = 0;
        break;
      case WM_LBUTTONDBLCLK:
      case WM_MBUTTONDBLCLK:
      case WM_RBUTTONDBLCLK:
      case WM_SIZE:
      case WM_NCDESTROY:
      case WM_SETFONT:
      case WM_NOTIFYFORMAT:
      case WM_QUERYUISTATE:
      case WM_CHANGEUISTATE:
      case WM_SHOWWINDOW:
      case WM_WINDOWPOSCHANGING:
      case WM_WINDOWPOSCHANGED:
      case WM_GETICON:
      case WM_NCCALCSIZE:
      case WM_NCACTIVATE:
      case WM_ACTIVATE:
      case WM_SETFOCUS:
      case WM_KILLFOCUS:
      case WM_NCPAINT:
      case WM_ERASEBKGND:
      case WM_POWERBROADCAST:
      case WM_DEVICECHANGE:
      case WM_ACTIVATEAPP:
      case WM_CTLCOLORBTN:
      case WM_CTLCOLORDLG:
      case WM_PAINT:
      case WM_CTLCOLORSTATIC:
      case WM_CTLCOLOREDIT:
      case WM_NCHITTEST:
      case WM_SETCURSOR:
      case WM_MOUSEMOVE:
      case WM_MOUSEACTIVATE:
      case WM_USER:
      case WM_NCMOUSEMOVE:
      case WM_NCMOUSELEAVE:
      case WM_NOTIFY:
      case WM_PARENTNOTIFY:
      case WM_NCLBUTTONDOWN:
      case WM_SYSCOMMAND:
      case WM_CAPTURECHANGED:
      case WM_GETMINMAXINFO:
      case WM_ENTERSIZEMOVE:
      case WM_MOVING:
      case WM_MOVE:
      case WM_EXITSIZEMOVE:
      case WM_UPDATEUISTATE:
      case WM_LBUTTONDOWN:
      case WM_LBUTTONUP:
      case WM_MBUTTONDOWN:
      case WM_MBUTTONUP:
      case WM_RBUTTONDOWN:
      case WM_RBUTTONUP:
      case WM_CANCELMODE:
        ret = 0;
        break;
    }
  }
  __except(CrashFunc(GetExceptionInformation())) {
    //  I don't want this dialog -- it's crashing!
    ::EndDialog(hDialog, 0);
  }

  return ret;
}

static BOOL CALLBACK ThreadWindowsProc(HWND hwnd, LPARAM)
{
  char buf[512];
  ::GetWindowText(hwnd, buf, 512);
  buf[511] = 0;
  if (hMainWindow == NULL || strstr(buf, ".dxs") != NULL) {
    hMainWindow = hwnd;
  }
#if !defined(NDEBUG)
  ::OutputDebugString((std::string("Top level window: ") + buf + "\n").c_str());
#endif
  return true;
}

/**This executes plugin code*/
void DLL_EXPORT __stdcall PluginExecute()
{
  if (hDialog == NULL) {
    //  let's assume the plug-in is called in the context of the main window
#if !defined(NDEBUG)
    ::OutputDebugString("LevelWalker: hooking GetMessage()\n");
#endif
    ::EnumThreadWindows(::GetCurrentThreadId(), ThreadWindowsProc, 0);
    hHook = ::SetWindowsHookEx(WH_GETMESSAGE, MessageProc, hModule, ::GetCurrentThreadId());
    hDialog = ::CreateDialog(hModule, MAKEINTRESOURCE(IDD_DIALOG1), 0, DlgProc);
    ::ShowWindow(hDialog, SW_SHOWNORMAL);
  }
  else {
    ::BringWindowToTop(hDialog);
  }
}

LRESULT DLL_EXPORT __stdcall PluginCommit()
{
  //  called when installer is done committing installation
  ::ShellExecute(0, "open", "http://www.mindcontrol.org/~hplus/graphics/levelwalker.html", 0, 0, SW_SHOW);
  return 0;
}

//  The hooking of the host application is somewhat hacky, 
//  but it seems to work OK.

static LRESULT CALLBACK MessageProc(int code, WPARAM wparam, LPARAM lparam)
{
  LRESULT ret = 0;
  __try {
    ret = ::CallNextHookEx(hHook, code, wparam, lparam);
    switch (code) {
      case HC_ACTION:
        if (wparam == PM_REMOVE) {
          MSG * msg = (MSG *)lparam;
          __try {
            if (msg->hwnd == hDialog && ::IsDialogMessage(hDialog, msg)) {
              switch (msg->message) {
                case WM_NCLBUTTONDOWN:
                case WM_NCMOUSEMOVE:
                case WM_NCLBUTTONUP:
                case WM_LBUTTONUP:
                case WM_MOUSEMOVE:
                  //  Don't nuke these messages; they are needed 
                  //  for drag-movement of dialogs. Bah!
                  break;
                default:
                  msg->message = WM_NULL;
                  msg->lParam = 0;
                  msg->wParam = 0;
                  break;
              }
            }
            else if (msg->hwnd == hMainWindow && msg->message == WM_DESTROY) {
              //  closing some window ... how do I know it's the main window?
              //  I just captured the top window when I was initialized...
              if (hDialog) {
                ::DestroyWindow(hDialog);
              }
            }
          }
          __except(EXCEPTION_EXECUTE_HANDLER) {
#if !defined(NDEBUG)
            ::OutputDebugString("LevelWalker ::IsDialogMessage() crashed!\n");
#endif
            ::DestroyWindow(hDialog);
            hDialog = 0;
            ::UnhookWindowsHookEx(hHook);
            hHook = 0;
          }
        }
        break;
      default:
        break;
    }
  }
  __except(CrashFunc(GetExceptionInformation())) {
    //  nothing I can do here, really...
  }
  return ret;
}

}//END EXTERN C


BOOL WINAPI DllMain(
  HANDLE hinstDLL, 
  DWORD dwReason, 
  LPVOID lpvReserved
)
{
  hModule = (HMODULE)hinstDLL;
  return TRUE;
}

